package com.zyl.blog.processing.service.impl;

import cn.hutool.core.lang.UUID;
import com.baomidou.mybatisplus.core.batch.MybatisBatch;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.base.MPJBaseServiceImpl;
import com.github.yulichang.query.MPJLambdaQueryWrapper;
import com.zyl.blog.common.domain.file.bean.AttachmentRelation;
import com.zyl.blog.common.domain.file.bean.FileStorage;
import com.zyl.blog.common.domain.file.bean.FileTransaction;
import com.zyl.blog.common.domain.file.dto.FileUploadResponse;
import com.zyl.blog.common.exception.BlogException;
import com.zyl.blog.common.utils.Extreme.IpUtils;
import com.zyl.blog.common.utils.Extreme.LogUtil;
import com.zyl.blog.processing.exception.FileProcessingException;
import com.zyl.blog.processing.exception.FileStorageException;
import com.zyl.blog.processing.mapper.AttachmentRelationMapper;
import com.zyl.blog.processing.mapper.FileStorageMapper;
import com.zyl.blog.processing.mapper.FileTransactionMapper;
import com.zyl.blog.processing.service.FileStorageService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import nl.bitwalker.useragentutils.UserAgent;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.chrono.ChronoLocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * @version v1.0
 * @ProjectName: Personal_Blog_Server
 * @Package: com.zyl.blog.processing.service.impl
 * @ClassName: IFileStorageService
 * @Description:
 * @Author FinalFantasy
 * @Date 2025/8/14-14:17
 */
@Service
@RequiredArgsConstructor
public class IFileStorageService extends MPJBaseServiceImpl<FileStorageMapper, FileStorage>
        implements FileStorageService {

    private final FileStorageMapper fileStorageMapper;
    private final FileTransactionMapper fileTransactionMapper;
    private final AttachmentRelationMapper attachmentRelationMapper;
    private final HttpServletRequest request;

    private final IpUtils ipUtils;

    private final SqlSessionFactory sqlSessionFactory;

    @Value("${file.storage.base-path}")
    private String fileStorageLocation;

    @Value("${file.storage.max-size}")
    private long maxFileSize;

    @Value("${file.storage.allowed-types}")
    private List<String> allowedFileTypes;

    @Override
    @Transactional
    public FileUploadResponse uploadFile(MultipartFile file, Long uploaderId, String description) {
        validateFile(file);

        try {
            /* 确保存储目录存在 */
            Path fileStoragePath = Paths.get(fileStorageLocation).toAbsolutePath().normalize();
            Files.createDirectories(fileStoragePath);

            /* 生成唯一文件名 */
            String originalFileName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
            String fileExtension = originalFileName.substring(originalFileName.lastIndexOf('.'));
            String fileName = UUID.randomUUID() + fileExtension;

            /* 保存文件到存储位置 */
            Path targetLocation = fileStoragePath.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            /* 计算文件MD5 */
            String md5Hash = calculateFileMd5(file);

            /* 检查是否已存在相同文件 */
            Optional<FileStorage> existingFile = fileStorageMapper.findByMd5Hash(md5Hash);
            if (existingFile.isPresent()) {
                /* 如果存在相同文件，只需创建引用而不是存储新文件 */
                FileStorage fileStorage = existingFile.get();
                recordFileTransaction(fileStorage.getId(), uploaderId, 1, 1, null);
                return mapToFileUploadResponse(fileStorage);
            }

            // 创建文件存储记录
            FileStorage fileStorage = new FileStorage();
            fileStorage
                    .setFileName(fileName)
                    .setOriginalFileName(originalFileName)
                    .setFileType(fileExtension.substring(1))
                    .setMimeType(file.getContentType())
                    .setFileSize(file.getSize())
                    .setStoragePath(targetLocation.toString())
                    .setStorageType("local")
                    .setMd5Hash(md5Hash)
                    .setUploaderId(uploaderId)
                    .setDescription(description);

            fileStorageMapper.insert(fileStorage);

            /* 记录文件上传交易 */
            recordFileTransaction(fileStorage.getId(), uploaderId, 1, 1, null);

            return mapToFileUploadResponse(fileStorage);
        } catch (IOException ex) {
            throw new FileStorageException("无法存储文件 " + file.getOriginalFilename() + ". 请再次尝试!", ex);
        }
    }

    @Override
    @Async("virtualThreadExecutor")
    @Transactional(rollbackFor = FileStorageException.class)
    public CompletableFuture<FileUploadResponse> uploadFileAsync(MultipartFile file, Long uploaderId, String description) {
        return CompletableFuture.supplyAsync(() -> uploadFile(file, uploaderId, description));
    }

    @Override
    @Transactional
    public List<FileUploadResponse> uploadFiles(List<MultipartFile> files, Long uploaderId, Map<String, String> descriptions) {
        return files.stream()
                .map(file -> {
                    String description = descriptions != null ? descriptions.getOrDefault(file.getOriginalFilename(), "") : "";
                    return uploadFile(file, uploaderId, description);
                })
                .collect(Collectors.toList());
    }

    @Override
    @Transactional(rollbackFor = BlogException.class)
    public Resource downloadFile(Long fileId, Long userId) {
        try {
            FileStorage fileStorage = fileStorageMapper
                    .findById(fileId)
                    .orElseThrow(() -> {
                        LogUtil.error("找不到ID为[{}]的文件", null, fileId);
                        return new FileStorageException("找不到ID为 " + fileId + " 的文件");
                    });

            /* 检查文件是否有效 */
            if (fileStorage.getStatus() != 1 ||
                    (fileStorage.getExpireTime() != null && fileStorage.getExpireTime().isBefore(ChronoLocalDateTime.from(new Date().toInstant())))) {
                throw new FileStorageException("文件不可用: ID = " + fileId);
            }

            Path filePath = Paths.get(fileStorage.getStoragePath()).normalize();
            Resource resource = new FileSystemResource(filePath);

            if (resource.exists()) {
                /* 记录下载交易 */
                recordFileTransaction(fileId, userId, 2, 1, null);
                return resource;
            } else {
                recordFileTransaction(fileId, userId, 2, 0, "文件不存在于存储位置");
                throw new FileStorageException("文件不存在于存储位置: " + fileStorage.getStoragePath());
            }
        } catch (Exception ex) {
            throw new FileStorageException("无法下载文件. ID = " + fileId, ex);
        }
    }

    @Override
    @Transactional(rollbackFor = BlogException.class)
    public FileStorage getFileInfo(Long fileId) {
        return fileStorageMapper.findById(fileId)
                .orElseThrow(() -> new FileStorageException("找不到ID为 " + fileId + " 的文件"));
    }

    @Override
    @Transactional(rollbackFor = BlogException.class)
    public boolean deleteFile(Long fileId, Long userId) {
        try {
            FileStorage fileStorage = fileStorageMapper.findById(fileId)
                    .orElseThrow(() -> new FileStorageException("找不到ID为 " + fileId + " 的文件"));

            /* 检查是否有其他引用 */
            long referenceCount = attachmentRelationMapper.countByFileId(fileId);
            if (referenceCount > 0) {
                /* 如果有引用，仅标记为禁用而非物理删除 */
                fileStorage.setStatus(0);
                fileStorageMapper.updateById(fileStorage);
            } else {
                /* 如果没有引用，删除文件记录和实际文件 */
                Path filePath = Paths.get(fileStorage.getStoragePath()).normalize();
                LogUtil.info("删除文件，路径: {}", filePath.toString());
                Files.deleteIfExists(filePath);

                /* 删除数据库记录 */
                fileStorageMapper.deleteById(fileStorage);
            }

            /* 记录删除交易 */
            recordFileTransaction(fileId, userId, 3, 1, null);
            return true;
        } catch (Exception ex) {
            recordFileTransaction(fileId, userId, 3, 0, ex.getMessage());
            throw new FileStorageException("无法删除文件. ID = " + fileId, ex);
        }
    }

    @Override
    @Transactional(rollbackFor = BlogException.class)
    public List<FileStorage> listFiles(int page, int size, String fileType, Long uploaderId) {
        LambdaQueryWrapper<FileStorage> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper
                .eq(fileType != null, FileStorage::getFileType, fileType)
                .eq(uploaderId != null, FileStorage::getUploaderId, uploaderId);

        return fileStorageMapper.selectPage(new Page<>(page, size), queryWrapper).getRecords();
    }

    @Override
    @Transactional
    public boolean attachFileToEntity(Long fileId, Long relationId, String relationType, boolean isPrimary) {
        /* 检查文件是否存在 */
        boolean fileExists = fileStorageMapper.exists(
                new LambdaQueryWrapper<FileStorage>().eq(FileStorage::getId, fileId)
        );
        
        if (!fileExists) {
            LogUtil.error("找不到ID为[{}]的文件", null, fileId);
            throw new FileStorageException("找不到ID为 " + fileId + " 的文件");
        }

        /* 如果设置为主附件，先取消同对象的其他主附件 */
        if (isPrimary) {
            List<AttachmentRelation> primaryAttachments =
                    attachmentRelationMapper.selectList(
                            new MPJLambdaQueryWrapper<AttachmentRelation>()
                                    .eq(AttachmentRelation::getRelationType, relationType)
                                    .eq(AttachmentRelation::getRelationId, relationId)
                                    .eq(AttachmentRelation::getIsPrimary, 1)
                    );

             MybatisBatch<AttachmentRelation> batch = new MybatisBatch<>(sqlSessionFactory, primaryAttachments);
             MybatisBatch.Method<AttachmentRelation> method = new MybatisBatch.Method<>(AttachmentRelationMapper.class);
             batch.execute(method.update(attach -> new LambdaUpdateWrapper<AttachmentRelation>()
                 .eq(AttachmentRelation::getId, attach.getId()) // 关键：绑定当前记录ID
                 .set(AttachmentRelation::getIsPrimary, 0)
             ));
        }

        /* 检查是否已存在关联 */
        AttachmentRelation existingRelation =
                attachmentRelationMapper
                        .selectOne(
                                new LambdaQueryWrapper<AttachmentRelation>()
                                        .eq(AttachmentRelation::getFileId, fileId)
                                        .eq(AttachmentRelation::getRelationId, relationId)
                                        .eq(AttachmentRelation::getRelationType, relationType)
                        );

        if (existingRelation != null) {
            /* 更新现有关联 */
            existingRelation.setIsPrimary(isPrimary ? 1 : 0);
            attachmentRelationMapper.updateById(existingRelation);
        } else {
            /* 创建新关联 */
            AttachmentRelation relation =
                    new AttachmentRelation()
                    .setFileId(fileId)
                    .setRelationId(relationId)
                    .setRelationType(relationType)
                    .setIsPrimary(isPrimary ? 1 : 0);

            /* 计算排序序号, 查询时加锁，防止并发冲突 */
            Long maxSortOrder = attachmentRelationMapper.selectObjs(
                            new LambdaQueryWrapper<AttachmentRelation>()
                                    .select(AttachmentRelation::getSortOrder)
                                    .eq(AttachmentRelation::getRelationType, relationType)
                                    .eq(AttachmentRelation::getRelationId, relationId)
                                    .last("ORDER BY sort_order DESC LIMIT 1 FOR UPDATE")  // 关键：加行锁
                    ).stream()
                    .findFirst()
                    .map(o -> (Long) o)
                    .orElse(0L);

            relation.setSortOrder((int) (maxSortOrder + 1));

            attachmentRelationMapper.insert(relation);
        }

        return true;
    }

    @Override
    @Transactional(rollbackFor = BlogException.class)
    public List<FileStorage> getAttachments(Long relationId, String relationType) {
        List<AttachmentRelation> relations =
                attachmentRelationMapper.selectList(
                        new MPJLambdaQueryWrapper<AttachmentRelation>()
                                .selectAll(AttachmentRelation.class)
                                .eq(AttachmentRelation::getRelationType, relationType)
                                .eq(AttachmentRelation::getRelationId, relationId)
                                .orderByAsc(AttachmentRelation::getSortOrder)
                );

        List<Long> fileIds = relations.stream()
                .map(AttachmentRelation::getFileId)
                .collect(Collectors.toList());

        return fileStorageMapper.selectByIds(fileIds);
    }

    @Override
    public String calculateFileMd5(MultipartFile file) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[8192];
            int read;

            try (var inputStream = file.getInputStream()) {
                while ((read = inputStream.read(buffer)) > 0) {
                    digest.update(buffer, 0, read);
                }
            }

            byte[] hashBytes = digest.digest();

            /* 转换为十六进制字符串 */
            StringBuilder sb = new StringBuilder();
            for (byte b : hashBytes) {
                sb.append(String.format("%02x", b));
            }

            return sb.toString();
        } catch (NoSuchAlgorithmException | IOException ex) {
            throw new FileProcessingException("无法计算文件MD5哈希值", ex);
        }
    }

    /**
     * 验证文件是否符合要求
     */
    private void validateFile(MultipartFile file) {
        /* 检查文件是否为空 */
        if (file.isEmpty()) {
            throw new FileStorageException("无法上传空文件");
        }

        /* 检查文件大小 */
        if (file.getSize() > maxFileSize) {
            throw new FileStorageException("文件大小超过限制. 最大允许大小: " + maxFileSize / (1024 * 1024) + "MB");
        }

        /* 检查文件类型 */
        String contentType = file.getContentType();
        if (contentType == null || !allowedFileTypes.contains(contentType)) {
            throw new FileStorageException("不支持的文件类型: " + contentType +
                    ". 支持的类型: " + String.join(", ", allowedFileTypes));
        }
    }

    /**
     * 记录文件操作交易
     */
    private void recordFileTransaction(Long fileId, Long userId, int transactionType,
                                       int status, String errorMessage) {
        try {
            FileTransaction transaction =
                    new FileTransaction()
                    .setFileId(fileId)
                    .setUserId(userId)
                    .setTransactionType(transactionType)
                    .setStatus(status)
                    .setErrorMessage(errorMessage);

            /* 获取客户端IP */
            String ipAddress = ipUtils.getClientIp(request);
            transaction.setIpAddress(ipAddress);

            /* 获取用户代理 */
            transaction.setUserAgent(UserAgent.parseUserAgentString(request.getHeader("User-Agent")).toString());

            transaction.setTransactionTime(LocalDateTime.now());

            /* 获取文件大小 */
            if (status == 1) {
                /* 操作成功时才记录文件大小 */
                fileStorageMapper.findById(fileId).ifPresent(file -> transaction.setFileSize(file.getFileSize()));
            }

            fileTransactionMapper.insert(transaction);
        } catch (Exception ex) {
            /* 记录失败不应影响主操作，仅日志记录 */
            LogUtil.error("记录文件失败：[{}]", null, ex.getMessage());
        }
    }

    /**
     * 转换为文件上传响应DTO
     */
    private FileUploadResponse mapToFileUploadResponse(FileStorage fileStorage) {
        return
                new FileUploadResponse()
                .setFileId(fileStorage.getId())
                .setFileName(fileStorage.getOriginalFileName())
                .setFileType(fileStorage.getFileType())
                .setFileSize(fileStorage.getFileSize())
                .setUploadTime(fileStorage.getUploadTime())
                .setDownloadUrl("/api/files/download/" + fileStorage.getId());
    }
}
